package complie;

import common.FileUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Created by cc
 * Description: 每次的 “编译 + 运行” 这个过程，就称为一个 complie.Task
 * User: CZH
 * Date: 2023-01-05
 * Time: 15:03
 */
public class Task {

    // 通过一组常类来约定临时文件的名字，好处：需要的文件太多太乱，集合在一起好打理；未来需要修改，也能一起修改；
    // 这个表示所有临时文件所在的目录
    private String WORK_DIR = null;
    // 约定代码的类名
    private String CLASS = null;
    // 约定要编译的代码文件名
    private String CODE = null;
    // 约定存放编译错误信息的文件名
    private String COMPILE_ERROR = null;
    // 约定存放运行时的标准输出的文件名
    private String STDOUT = null;
    // 约定存放运行时的标准错误的文件名
    private String STDERR = null;

    public Task() {
        // 在 Java 中使用 UUID 这个类就能生成一个 UUID
        WORK_DIR = "./tmp/" + UUID.randomUUID().toString() + "/";
        CLASS = "Solution";
        CODE = WORK_DIR + "Solution.java";
        COMPILE_ERROR = WORK_DIR + "compileError.txt";
        STDOUT = WORK_DIR + "stdout.txt";
        STDERR = WORK_DIR + "stderr.txt";
    }

    /**
     *  complie.Task 的核心方法，编译 + 运行
     *  参数： 要编译运行的 Java 源代码
     *  返回值：表示编译运行的结果，编译出错/运行出错/运行正确...
     */
    public Answer compileAndRun(Question question) {
        Answer answer = new Answer();

        // 0. 准备好用来存放临时文件的目录
        File workDir = new File(WORK_DIR);
        //如果该目录不存在，就创建该目录
        if (!workDir.exists()) {
            workDir.mkdirs();
        }

        // 0.1. 进行安全性判断
        if (!checkCodeSafe(question.getCode())) {
            System.out.println("用户提交了不安全的代码！");
            answer.setError(3);
            answer.setReason("您提交的代码不安全~");
            return answer;
        }

        // 1. 把 question 中的 code 写入到一个 Solution.java 文件中。
        //      类似力扣要求用户把代码写到一个 Solution 的类中，类名和文件名一致，才能使用 javac 来编译
        FileUtil.writeFile(CODE, question.getCode());

        // 2. 创建子进程，调用 javac 对 .java 文件进行编译
        //      如果编译出错，javac 会把错误信息写入到 stderr 里，使用一个专门的文件来保存 compileError.txt
        //      需要先把编译命令构造出来.
        String compileCmd = String.format("javac -encoding utf8 %s -d %s", CODE, WORK_DIR);
        System.out.println("编译命令：" + compileCmd);
        //创建子进程，执行 javac 命令
        CommandUtil.run(compileCmd, null, COMPILE_ERROR);
        //如果编译出错，错误信息就会被记录到 COMPILE_ERROR 这个文件中；如果没有编译出错，则文件为空
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if (!compileError.equals("")) {
            //编译出错，让 answer 记录错误信息并返回
            System.out.println("编译出错！");
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }

        // 3. 创建子进程，调用 java 命令执行编译后的 .class 文件
        //      如果执行报错，就把 java 子进程的标准输出和标准错误获取到 stdout.txt / stderr.txt
        String runCmd = String.format("java -classpath %s %s", WORK_DIR, CLASS);
        System.out.println("运行命令：" + runCmd);
        CommandUtil.run(runCmd, STDOUT, STDERR);
        String runError = FileUtil.readFile(STDERR);
        if (!runError.equals("")) {
            System.out.println("运行出错！");
            answer.setError(2);
            answer.setReason(runError);
            return answer;
        }

        // 4. 父进程获取到刚才的编译执行结果，并打包成 complie.Answer 对象
        //      编译执行的结果，就通过刚才约定的这几个文件来进行获取即可.
        answer.setError(0);
        answer.setStdout(FileUtil.readFile(STDOUT));
        return answer;
    }

    private boolean checkCodeSafe(String code) {
        List<String> blackList = new ArrayList<>();
        // 恶意代码
        blackList.add("Runtime");
        blackList.add("exec");
        // 禁止读写文件
        blackList.add("java.io");
        // 禁止访问网络
        blackList.add("java.net");

        for (String target : blackList) {
            int pos = code.indexOf(target);
            if (pos >= 0) {
                // 找到任意恶意代码的特征，返回 false 表示不安全
                return false;
            }
        }
        return true;
    }


    public static void main(String[] args) {
        Task task = new Task();
        Question question = new Question();
        question.setCode("public class Solution {\n" +
                "    public static void main(String[] args) {\n" +
                "        System.out.println(\"hello Solution\");\n" +
                "    }\n" +
                "}");
        Answer answer = task.compileAndRun(question);
        System.out.println(answer);
    }
}

